package nels4561;

import java.util.ArrayList;

/**
 * Our learning object, an Artificial Neural Network
 * This is a feed-forward NN using Backpropagation.
 * The inputs are the x, y of the ship and the x,y of the target
 * The outputs are the angular acceleration, the translational angle,
 * and the translational magnitude.
 * 
 * @author Michael
 */
public class NeuralNetwork {
	
	/**
	 * The input layer
	 */
	private Neuron[] inputLayer;
	/**
	 * The hidden layer
	 */
	private Neuron[] hiddenLayer;
	/**
	 * The output layer
	 */
	private Neuron[] outputLayer;
	
	/**
	 * Step size for back propagation
	 */
	private static final double stepSize = .02;
	/**
	 * Values calulated to correct weights
	 */
	private static double[] sigY;
	private static double[] sigO;
	
	
	/**
	 * The constructor for when we are done learning.
	 * This will pull the weights stored in LearnNeuralNetwork.java
	 * and create the NN based on those.
	 * @param topology
	 */
	public NeuralNetwork(int[] topology)
	{
		//Call the base constructor
		this(topology[0], topology[1],topology[2]);
		
		//Set the learned weights
		setLearnedWeights();
	}
	
	/**
	 * Standard constructor. This is what we used the first train the NN
	 * @param inputSize
	 * @param hiddenSize
	 * @param outputSize
	 */
	public NeuralNetwork(int inputSize, int hiddenSize, int outputSize)
	{
		//Should all be self explanatory
		inputLayer = new Neuron[inputSize];
		hiddenLayer = new Neuron[hiddenSize];
		outputLayer = new Neuron[outputSize];
		
		sigY = new double[hiddenSize];
		sigO = new double[outputSize];
		
		for (int i = 0; i < inputSize; i++)
		{
			inputLayer[i] = new Neuron();
		}
		
		for (int i = 0; i < hiddenSize; i++)
		{
			hiddenLayer[i] = new Neuron();
		}
		
		for (int i = 0; i < outputSize; i++)
		{
			outputLayer[i] = new Neuron();
		}
		
		setSynapses();
	}
	
	/**
	 * Sets the weights stored in LearnedNeuralNetwork.java
	 */
	public void setLearnedWeights()
	{
		//The weights are stored in an array of string arrays. The outer layer
		//is for an individual node, the inner layer is for the multiple
		//synapses it has.
		String[][] hiddenNodes = LearnedNeuralNetwork.getHiddenNodes();
		String[][] outputNodes = LearnedNeuralNetwork.getOutputNodes();
		
		//Pull and set the hidden nodes
		for (int i = 0; i < hiddenNodes.length; i++)
		{
			String[] stringNode = hiddenNodes[i];
			Neuron actualNode = hiddenLayer[i];
			
			ArrayList<Synapse> ss = actualNode.getSynapses();
			
			for (int j = 0; j < stringNode.length; j++)
			{
				double weight = Double.parseDouble(stringNode[j]);

				Synapse s = ss.get(j);
				s.setWeight(weight);
			}
		}
		
		//Pull and set the output nodes
		for (int i = 0; i < outputNodes.length; i++)
		{
			String[] stringNode = outputNodes[i];
			Neuron actualNode = outputLayer[i];
			
			ArrayList<Synapse> ss = actualNode.getSynapses();
			
			for (int j = 0; j < stringNode.length; j++)
			{
				double weight = Double.parseDouble(stringNode[j]);

				Synapse s = ss.get(j);
				s.setWeight(weight);
			}
		}
	}
	
	/**
	 * After creating the neurons, this method connects them all together
	 * appropriately. 
	 */
	public void setSynapses()
	{
		for (int i = 0; i < inputLayer.length; i++)
		{
			for (int j = 0; j < hiddenLayer.length; j++)
			{
				Synapse s = new Synapse(inputLayer[i], hiddenLayer[j]);
				
				hiddenLayer[j].addInputSynapse(s);
			}
		}
		
		for (int i = 0; i < hiddenLayer.length; i++)
		{
			for (int j = 0; j < outputLayer.length; j++)
			{
				Synapse s = new Synapse(hiddenLayer[i], outputLayer[j]);
				
				outputLayer[j].addInputSynapse(s);
			}
		}
	}
	
	/**
	 * Set the input nodes to the x,y of the ship and the
	 * x,y of the target
	 * @param coords
	 */
	public void setInputs(double[] coords)
	{
		if (coords.length != inputLayer.length)
		{
			return;
		}
		
		for (int i = 0; i < coords.length; i++)
		{
			Neuron node = inputLayer[i];
			node.setOutput(coords[i]);
		}
	}
	
	/**
	 * Has each hidden node sum its inputs and then 
	 * each output node sum its inputs. The reulting output
	 * node outputs are the angular velocity, translational
	 * angle and translational magnitude
	 * @return
	 */
	public Neuron[] processNetwork()
	{
		for (int i = 0; i < hiddenLayer.length; i++)
		{
			hiddenLayer[i].calculateOutput();
		}
		
		for (int i = 0; i < outputLayer.length; i++)
		{
			outputLayer[i].calculateOutput();
		}
		
		return outputLayer;
	}
	
	/**
	 * The actual learning method. Uses a slightly
	 * modified backpropagation method.
	 * @param target
	 * @param actual
	 */
	public void learn(double[] target, double[] actual)
	{
		//These sigma arrays are required for the weight updating
		//To learn more about these, read up on backpropagation
		calculateSigO(target, actual);
		calculateSigY();
		
		updateHiddenWeights();
		updateOutputWeights();
	}
	
	/**
	 * Part of backpropagation. Evolves the hidden 
	 * node weights, using the sigma Y array.
	 */
	public void updateHiddenWeights()
	{
		for (int i = 0; i < hiddenLayer.length; i++)
		{
			ArrayList<Synapse> ss = hiddenLayer[i].getSynapses();
			
			for ( Synapse s : ss)
			{
				double w = s.getWeight();
				
				w += (-stepSize * sigY[i] * w);
				
				s.setWeight(w);
				
				//System.out.println(w);
			}
		}
	}
	
	/**
	 * Part of backpropagation. Evolves the output 
	 * node weights, using the sigma O array.
	 */
	public void updateOutputWeights()
	{
		for (int i = 0; i < outputLayer.length; i++)
		{
			ArrayList<Synapse> ss = outputLayer[i].getSynapses();
			
			for ( Synapse s : ss)
			{
				double w = s.getWeight();
				
				w += (-stepSize * sigO[i] * w);
				
				s.setWeight(w);
				
				//System.out.println(w);
			}
		}
	}
	
	/**
	 * Calulates the sigma Y array. Added in some of my 
	 * own math to accomodate for the fact that we aren't
	 * using the standard activation functions.
	 */
	public void calculateSigY()
	{
		for ( int i = 0; i < hiddenLayer.length; i++)
		{
			sigY[i] = 0;
			
			double sum = 0;
			
			for (int j = 0; j < outputLayer.length; j++)
			{
				Neuron oNode = outputLayer[j];
				ArrayList<Synapse> ss = oNode.getSynapses();
				Synapse s = ss.get(i);
				
				double fyj = (1 - 1/hiddenLayer[i].getOutput()) * 1/hiddenLayer[i].getOutput();
				
				sum += sigO[j] * s.getWeight() * fyj;
			}
			
			sigY[i] += sum;
			
			//System.out.println(sigY[i]);
		}
	}
	
	/**
	 * Calulates the sigma O array. Added in some of my 
	 * own math to accomodate for the fact that we aren't
	 * using the standard activation functions.
	 */
	public void calculateSigO(double[] target, double[] actual)
	{
		for (int i = 0; i < target.length; i++)
		{
			sigO[i] = -(target[i] - actual[i]) * (1 - 1/actual[i]) * 1/actual[i];
		}
	}
	
	/**
	 * Calculates the error using mean squared error. Will use this to prove
	 * that we are actually learning.
	 * @param target
	 * @param actual
	 * @return
	 */
	public double calculateError(double[] target, double[] actual)
	{
		double num = Math.pow((target[0] - actual[0]), 2) + Math.pow((target[1] - actual[1]), 2) + Math.pow((target[2]-actual[2]), 2);
		
		return num/6;
	}
	
	/**
	 * Getter for the calculated outputs
	 * @return
	 */
	public double[] getOutputs()
	{
		return new double[] {outputLayer[0].getOutput(), outputLayer[1].getOutput(), outputLayer[2].getOutput()};
	}
	
	/**
	 * Prints the weights for each node.
	 */
	public void printWeights()
	{
		for (int i = 0; i < hiddenLayer.length; i++)
		{
			Neuron n = hiddenLayer[i];
			ArrayList<Synapse> ss = n.getSynapses();
			
			System.out.println("Hidden Node: " + i);
			
			for (int j = 0; j < ss.size(); j++)
			{
				System.out.println("\"" + ss.get(j).getWeight() + "\",");
			}
					
		}
		
		for (int i = 0; i < outputLayer.length; i++)
		{
			Neuron n = outputLayer[i];
			ArrayList<Synapse> ss = n.getSynapses();
			
			System.out.println("Output Node: " + i);
			
			for (int j = 0; j < ss.size(); j++)
			{
				System.out.println("\"" + ss.get(j).getWeight() + "\",");
			}
					
		}
	}

}
